/*
 * Background Flickr: Copyright (C) 2009  Craig McNicholas
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.ddw.app.bflickr.flickr.api.request;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;

import com.ddw.app.bflickr.flickr.api.FlickrRequest;
import com.ddw.app.bflickr.flickr.api.FlickrRequestListener;
import com.ddw.app.bflickr.flickr.api.FlickrResponse;
import com.ddw.app.bflickr.flickr.api.FlickrURL;
import com.ddw.app.bflickr.logging.Log;
import com.ddw.app.bflickr.logging.LogFactory;

/**
 * Abstract class to build and execute an asynchronous request as 
 * well as raising callbacks to attached listeners of the request 
 * when it completes or errors.
 * @author craig
 *
 */
public abstract class FlickrApiRequest implements FlickrRequest {

	private Log log = LogFactory.createInstance(this.getClass());
	private FlickrURL url = null;
	protected ArrayList<FlickrRequestListener> listeners = new ArrayList<FlickrRequestListener>();
	
	/**
	 * Creates a new api request instance to request a response from a 
	 * REST url and provide callbacks to handle to returned data.
	 * @param url the url to request a response from
	 * @throws IllegalArgumentException when the url is null, <i>url's cannot be null</i>
	 */
	public FlickrApiRequest(FlickrURL url) throws IllegalArgumentException {
		setUrl(url);
	}
	
	@Override
	public void addListener(FlickrRequestListener listener) {
		if(listener != null) {
			if(!this.listeners.contains(listener)) {
				this.listeners.add(listener);
			} else {
				log.debug("Listeners list already contains listener " + listener);
			}
		} else {
			log.warn("A null listener cannot be added");
		}
	}

	@Override
	public void clearListeners() {
		this.listeners.clear();
	}

	@Override
	public void removeListener(FlickrRequestListener listener) {
		if(listener != null) {
			if(this.listeners.contains(listener)) {
				this.listeners.remove(listener);
			} else {
				log.warn("Listeners list does not contain listener " + listener);
			}
		} else {
			log.warn("A null listener cannot be removed");
		}
	}

	@Override
	public synchronized void setUrl(FlickrURL url) {
		if(url == null) {
			log.error("The flickr url cannot be null");
			throw new IllegalArgumentException("The flickr url cannot be null");
		}
		this.url = url;
	}
	
	@Override
	public synchronized FlickrURL getUrl() {
		return this.url;
	}
	
	@Override
	public void raiseOnRequestErrorEvent(Throwable throwable) {
		log.debug("Raising request error event");
		for(FlickrRequestListener listener : this.listeners) {
			if(listener != null) {
				listener.onRequestError(throwable);
			} else {
				log.warn("Cannot raise event on a null listener");
			}
		}
	}

	@Override
	public void raiseOnCompleted(FlickrApiOnRequestCompletedEventArgs data) {
		log.debug("Raising request completed event");
		for(FlickrRequestListener listener : this.listeners) {
			if(listener != null) {
				listener.onRequestCompleted(data);
			} else {
				log.warn("Cannot raise event on a null listener");
			}
		}
	}
	
	@Override
	public void raiseOnParseError(Throwable throwable) {
		log.debug("Raising parse error event");
		for(FlickrRequestListener listener : this.listeners) {
			if(listener != null) {
				listener.onParseError(throwable);
			} else {
				log.warn("Cannot raise event on a null listener");
			}
		}
	}

	@Override
	public void start() {
		log.debug("Starting asynchronous thread");
		new Thread(new Runnable() {

			@Override
			public void run() {
				log.debug("Running asynchronous request");
				try {
					log.debug("Sending request");
					final String response = sendRequest();
					log.debug("Response successful");
					try {
						final FlickrResponse data = parseResponse(response);
						final boolean hasResponse = response != null && data != null ? response.length() > 0 : false;
						raiseOnCompleted(new FlickrApiOnRequestCompletedEventArgs() {
							
							@Override
							public String getResponseString() {
								return response;
							}
	
							@Override
							public boolean hasResponse() {
								return hasResponse;
							}
	
							@Override
							public FlickrResponse getResponse() {
								return data;
							}
							
						});
					} catch(Throwable ex) {
						log.error("There was an error parsing the response", ex);
						raiseOnParseError(ex);
					}
				} catch(Throwable ex) {
					log.error("There was an error requesting from the service", ex);
					raiseOnRequestErrorEvent(ex);
				}
				log.debug("Ending asynchronous request");
			}
			
		}).run();
	}
	
	/**
	 * Function to parse the response retrieved from the request. This 
	 * method must be written for each instance of the flickr request 
	 * as each request can contain a different response.
	 * @param response the response string received from the request
	 * @return a flickr response object containing the data returned
	 * @throws Throwable this can occur if a parsing error takes place
	 */
	protected abstract FlickrResponse parseResponse(String response) throws Throwable;
	
	/**
	 * Sends a request and captures the response outputting it in a string.
	 * @return returns the response as a string
	 * @throws MalformedURLException when the url is badly formed
	 * @throws UnsupportedEncodingException when the utf-8 encoding format is not supported
	 * @throws IOException if there is a problem opening a connection or reading from the stream
	 */
	protected String sendRequest() throws MalformedURLException, UnsupportedEncodingException, IOException {
		log.debug("Opening stream");
		final URL url = new URL(getUrl().getUrl());
        InputStream inStream = url.openStream();
        String response = null;
        try {
        	log.debug("Creating stream reader");
        	InputStreamReader inReader = new InputStreamReader(inStream, "UTF-8"); // All flickr api's use utf-8 as default
        	try {
        		response = readResponse(inReader);
        	} catch(IOException ex) {
        		log.error("There was an error reading from the stream", ex);
        		throw ex;
        	} catch(IndexOutOfBoundsException ex) {
        		log.error("There was an error indexing the buffer", ex);
        		throw ex;
        	} finally {
        		log.debug("Closing stream reader");
        		inReader.close();
        	}
        } catch(IOException ex) {
    		log.error("There was an error opening the stream", ex);
        	throw ex;
        } finally {
    		log.debug("Closing stream");
        	inStream.close();
        }
        return response;
	}

	/**
	 * Reads the response from a request and return it as a string
	 * @param in the stream to read the response from
	 * @return a string containing the response
	 * @throws IOException if there is a problem reading from the stream
	 */
	protected String readResponse(InputStreamReader in) throws IOException {
		StringBuilder builder = new StringBuilder();
		final int bufferLength = 1024;
		char[] buffer = new char[bufferLength];
		int charactersRead = 0;
		log.debug("Beginning stream read to buffer");
		while(charactersRead != -1) {
			charactersRead = in.read(buffer, 0, bufferLength);
			log.debug("Read " + charactersRead + " characters from the stream.");
			if(charactersRead > 0) {
				builder.append(buffer, 0, charactersRead);
			}
		}
		log.debug("Completed stream read to buffer");
		return builder.toString();
	}

}
